"use strict";
/**
 * @description parameter validator
 * @author LeaFish <735683662@qq.com>
 * @date 2020-05-15
 * @class Validator
 */
class Validator {
  constructor(options) {
    const {
      customMatchRules = {},
      convert = true,
      throwError = true,
      trim = true,
    } = options || {};

    // ruleSet
    // {required format type options,convert,message,default}

    // default match rules
    this.matchRules = {
      string: {
        format: (value) => typeof value === "string",
        convert: (value) => String(value),
      },
      int: {
        format: /^([0-9]+|0)$/,
        convert: (value) => Number(value),
      },
      double: {
        format: /^(([0-9]+|0)\.([0-9]{1,}))$/,
        convert: (value) => Number(value),
      },
      number: {
        format: /^(([0-9]+|0)\.([0-9]{1,})$)|^([0-9]+|0)$/,
        convert: (value) => Number(value),
      },
      date: {
        format: /^\d{4}\-\d{2}\-\d{2}$/,
        convert: (value) => new Date(value),
      },
      datetime: {
        format: /^\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}$/,
        convert: (value) => new Date(value),
      },
      tel: {
        format: /^1\d{10}$/,
        convert: (value) => value,
      },
      email: {
        format: /^[a-z0-9\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+(?:\.[a-z0-9\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+)*@(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?$/,
        convert: (value) => value,
      },
      enmu: {
        format: (value, ruleSet) =>
          new RegExp(ruleSet.options.join("|")).test(value),
        convert: (value, ruleSet) =>
          ruleSet.options.find((item) => String(item) === String(value)),
      },
      any: {
        format: () => true,
        convert: (value) => value,
      },
    };

    // console.log(Object.keys(this.matchRules));

    this.convert = convert; // auto convert data type
    this.throwError = throwError; // throw error set
    this.trim = trim;

    Object.assign(this.matchRules, customMatchRules);
  }

  isEmpty(value) {
    return value === undefined || value === null || value === "";
  }

  isObject(value) {
    return Object.prototype.toString.call(value) === "[object Object]";
  }

  /**
   * @description parse rule, return ruleSet
   * @author LeaFish <735683662@qq.com>
   * @date 2020-05-15
   * @param {string} [rule=""]
   * @return
   * @memberof Validator
   */
  parseRule(rule = "") {
    const ruleSet = {
      required: true,
      type: "", // rule type
      options: [], // enmu options
      format: null,
      convert: null,
      message: "",
      isArray: false,
      rename: "", // rename field
    };

    // console.log("rule", rule);
    if (Array.isArray(rule)) {
      ruleSet.type = "enmu";
      ruleSet.options = rule;
    }

    if (typeof rule === "string") {
      if (rule.slice(-1) === "?") {
        ruleSet.required = false;
        ruleSet.type = rule === "?" ? "any" : rule.slice(0, -1);
      } else {
        ruleSet.type = rule;
      }
      // check array rule
      if (ruleSet.type.slice(0, 1) === "[" && ruleSet.type.slice(-1) === "]") {
        ruleSet.type = ruleSet.type.slice(1, -1);
        ruleSet.isArray = true;
      }
    }

    // check enmu type
    if (ruleSet.type.includes("|")) {
      ruleSet.options = ruleSet.type.split("|");
      ruleSet.type = "enmu";
    }

    // rule is a ruleSet
    if (this.isObject(rule)) {
      Object.assign(ruleSet, rule);
    }
    if (ruleSet.format === null) {
      if (!this.matchRules[ruleSet.type]) {
        return { error: `matchRules [${ruleSet.type}] not exists` };
      }
      Object.assign(ruleSet, ruleSet.type ? this.matchRules[ruleSet.type] : {});
    }

    if (!(ruleSet.convert instanceof Function)) {
      ruleSet.convert = (value) => value; // return self
    }
    return ruleSet;
  }

  /**
   * @description validate
   * @author LeaFish <735683662@qq.com>
   * @date 2020-05-15
   * @param {*} [rules={}]
   * @param {*} [data={}]
   * @param {Boolean} [mergeError]
   * @return {Object}
   * @memberof Validator
   */
  validate(rules = {}, data = {}, mergeError = true) {
    const newData = {};
    const errors = [];

    for (const k in rules) {
      const ruleSet = this.parseRule(rules[k]);
      if (ruleSet.error) {
        errors.push(ruleSet.error);
        continue;
      }

      // array data
      if (ruleSet.isArray) {
        if (!Array.isArray(data[k]) && !this.isEmpty(data[k])) {
          data[k] = [data[k]];
        }
      } else {
        if (Array.isArray(data[k])) {
          data[k] = data[k][0];
        }
      }

      if (this.trim && typeof data[k] === "string") {
        data[k] = data[k].trim();
      }

      const result = this.check(ruleSet, data[k], k);
      if (result !== true) {
        if (this.throwError && !mergeError) {
          throw result;
        } else {
          errors.push(result);
        }
        continue;
      }

      if (data[k] === undefined) {
        newData[k] =
          ruleSet.default === undefined ? undefined : ruleSet.default;
      } else {
        if (this.convert) {
          newData[k] = ruleSet.isArray
            ? data[k].map((item) => ruleSet.convert(item, ruleSet))
            : ruleSet.convert(data[k], ruleSet);
        } else {
          newData[k] = data[k];
        }
      }
      // rename field
      if (typeof ruleSet.rename === "string" && ruleSet.rename.length > 0) {
        newData[ruleSet.rename] = newData[k];
        delete newData[k];
      }
    }

    if (errors.length === 0) {
      return newData;
    }
    const err = {
      code: "invalid_param",
      message: "Validation Failed",
      errors,
    };

    if (this.throwError) {
      throw err;
    }

    return err;
  }

  /**
   * @description check data by ruleSet
   * @author LeaFish <735683662@qq.com>
   * @date 2020-05-15
   * @param {*} [ruleSet={}]
   * @param {*} value
   * @return
   * @memberof Validator
   */
  check(ruleSet = {}, value, field) {
    let isOk = false;

    // check required
    if (
      this.isEmpty(value) ||
      (ruleSet.isArray && Array.isArray(value) && value.length === 0)
    ) {
      return ruleSet.required
        ? {
            code: "missing_field",
            field,
            message: field + " required",
          }
        : true;
    }

    const validate = (val) => {
      if (ruleSet.format instanceof Function) {
        isOk = ruleSet.format(val, ruleSet);
      }

      if (ruleSet.format instanceof RegExp) {
        isOk = ruleSet.format.test(val);
      }

      if (!isOk) {
        const message =
          ruleSet.type !== "enmu"
            ? `should be ${
                /a|e|i|o|u/.test(ruleSet.type.slice(0, 1)) ? "an" : "a"
              } ${ruleSet.type}`
            : `should be one of ${ruleSet.options.join(",")}`;
        return {
          code: "invalid",
          field,
          message: ruleSet.message || message,
          value: val,
        };
      }
    };

    if (ruleSet.isArray) {
      for (let i = 0; i < value.length; i++) {
        const error = validate(value[i]);
        if (error) return error;
      }
    } else {
      const error = validate(value);
      if (error) return error;
    }

    return true;
  }
}

module.exports = Validator;
